<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<style>
  * {
    box-sizing: border-box;
  }

  :root {
    height: 100%;
    font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
    line-height: 1.5;
    font-weight: 400;

    background: #87a5b2;

    font-synthesis: none;
    text-rendering: optimizeLegibility;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    -webkit-text-size-adjust: 100%;
  }

  body {
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  .list {
    display: flex;
    flex-direction: column;
    max-width: 500px;
    width: 100%;
    gap: 10px 0;
  }

  .list__item {
    width: 100%;
    background: white;
    padding: 15px;
    border-radius: 5px;
    color: #001d29;
    font-weight: 500;
    font-size: 18px;
    box-shadow: 0 4px 6px -1px #001d2910, 0 2px 4px -2px #001d2910;
    display: flex;
    align-items: center;
    position: relative;
    will-change: transform;
  }

  .drag-handle {
    position: absolute;
    right: 0;
    width: 44px;
    height: 44px;
    display: flex;
    justify-content: center;
    align-items: center;
  }

  .drag-handle::after {
    content: '⠿';
    font-size: 25px;
    color: #00000099;
  }

  .list__item.is-idle .drag-handle {
    cursor: grab;
  }

  .list__item.is-idle {
    transition: 0.25s ease transform;
  }

  .list__item.is-draggable,
  .list__item.is-draggable .drag-handle {
    cursor: grabbing;
  }

  .list__item.is-draggable {
    z-index: 10;
  }
</style>

<body>
  <div class="list js-list">
    <div class="list__item is-idle js-item">
      🍦
      <div class="drag-handle js-drag-handle"></div>
    </div>
    <div class="list__item is-idle js-item">
      🥞
      <div class="drag-handle js-drag-handle"></div>
    </div>
    <div class="list__item is-idle js-item">
      🧇
      <div class="drag-handle js-drag-handle"></div>
    </div>
    <div class="list__item is-idle js-item">
      🍰
      <div class="drag-handle js-drag-handle"></div>
    </div>
  </div>
</body>


<script>
  /***********************
 *      Variables       *
 ***********************/

  let listContainer

  let draggableItem

  let pointerStartX
  let pointerStartY

  let itemsGap = 0

  let items = []

  /***********************
   *    Helper Functions   *
   ***********************/

  function getAllItems() {
    if (!items?.length) {
      items = Array.from(listContainer.querySelectorAll('.js-item'))
    }
    return items
  }

  function getIdleItems() {
    return getAllItems().filter((item) => item.classList.contains('is-idle'))
  }

  function isItemAbove(item) {
    return item.hasAttribute('data-is-above')
  }

  function isItemToggled(item) {
    return item.hasAttribute('data-is-toggled')
  }

  /***********************
   *        Setup        *
   ***********************/

  function setup() {
    listContainer = document.querySelector('.js-list')

    if (!listContainer) return

    listContainer.addEventListener('mousedown', dragStart)
    listContainer.addEventListener('touchstart', dragStart)

    document.addEventListener('mouseup', dragEnd)
    document.addEventListener('touchend', dragEnd)
  }

  /***********************
   *     Drag Start      *
   ***********************/

  function dragStart(e) {
    if (e.target.classList.contains('js-drag-handle')) {
      draggableItem = e.target.closest('.js-item')
    }

    if (!draggableItem) return

    pointerStartX = e.clientX || e.touches[0].clientX
    pointerStartY = e.clientY || e.touches[0].clientY

    setItemsGap()
    disablePageScroll()
    initDraggableItem()
    initItemsState()

    document.addEventListener('mousemove', drag)
    document.addEventListener('touchmove', drag, {passive: false})
  }

  function setItemsGap() {
    if (getIdleItems().length <= 1) {
      itemsGap = 0
      return
    }

    const item1 = getIdleItems()[0]
    const item2 = getIdleItems()[1]

    const item1Rect = item1.getBoundingClientRect()
    const item2Rect = item2.getBoundingClientRect()

    itemsGap = Math.abs(item1Rect.bottom - item2Rect.top)
  }

  function disablePageScroll() {
    document.body.style.overflow = 'hidden'
    document.body.style.touchAction = 'none'
    document.body.style.userSelect = 'none'
  }

  function initItemsState() {
    getIdleItems().forEach((item, i) => {
      if (getAllItems().indexOf(draggableItem) > i) {
        item.dataset.isAbove = ''
      }
    })
  }

  function initDraggableItem() {
    draggableItem.classList.remove('is-idle')
    draggableItem.classList.add('is-draggable')
  }

  /***********************
   *        Drag         *
   ***********************/

  function drag(e) {
    if (!draggableItem) return

    e.preventDefault()

    const clientX = e.clientX || e.touches[0].clientX
    const clientY = e.clientY || e.touches[0].clientY

    const pointerOffsetX = clientX - pointerStartX
    const pointerOffsetY = clientY - pointerStartY

    draggableItem.style.transform = `translate(${pointerOffsetX}px, ${pointerOffsetY}px)`

    updateIdleItemsStateAndPosition()
  }

  function updateIdleItemsStateAndPosition() {
    const draggableItemRect = draggableItem.getBoundingClientRect()
    const draggableItemY = draggableItemRect.top + draggableItemRect.height / 2

    // Update state
    getIdleItems().forEach((item) => {
      const itemRect = item.getBoundingClientRect()
      const itemY = itemRect.top + itemRect.height / 2
      if (isItemAbove(item)) {
        if (draggableItemY <= itemY) {
          item.dataset.isToggled = ''
        } else {
          delete item.dataset.isToggled
        }
      } else {
        if (draggableItemY >= itemY) {
          item.dataset.isToggled = ''
        } else {
          delete item.dataset.isToggled
        }
      }
    })

    // Update position
    getIdleItems().forEach((item) => {
      if (isItemToggled(item)) {
        const direction = isItemAbove(item) ? 1 : -1
        item.style.transform = `translateY(${direction * (draggableItemRect.height + itemsGap)
          }px)`
      } else {
        item.style.transform = ''
      }
    })
  }

  /***********************
   *      Drag End       *
   ***********************/

  function dragEnd() {
    if (!draggableItem) return

    applyNewItemsOrder()
    cleanup()
  }

  function applyNewItemsOrder() {
    const reorderedItems = []

    getAllItems().forEach((item, index) => {
      if (item === draggableItem) {
        return
      }
      if (!isItemToggled(item)) {
        reorderedItems[index] = item
        return
      }
      const newIndex = isItemAbove(item) ? index + 1 : index - 1
      reorderedItems[newIndex] = item
    })

    for (let index = 0; index < getAllItems().length; index++) {
      const item = reorderedItems[index]
      if (typeof item === 'undefined') {
        reorderedItems[index] = draggableItem
      }
    }

    reorderedItems.forEach((item) => {
      listContainer.appendChild(item)
    })
  }

  function cleanup() {
    itemsGap = 0
    items = []
    unsetDraggableItem()
    unsetItemState()
    enablePageScroll()

    document.removeEventListener('mousemove', drag)
    document.removeEventListener('touchmove', drag)
  }

  function unsetDraggableItem() {
    draggableItem.style = null
    draggableItem.classList.remove('is-draggable')
    draggableItem.classList.add('is-idle')
    draggableItem = null
  }

  function unsetItemState() {
    getIdleItems().forEach((item, i) => {
      delete item.dataset.isAbove
      delete item.dataset.isToggled
      item.style.transform = ''
    })
  }

  function enablePageScroll() {
    document.body.style.overflow = ''
    document.body.style.touchAction = ''
    document.body.style.userSelect = ''
  }

  /***********************
   *      Start Here     *
   ***********************/

  setup()
</script>

</html>